﻿/* 
* Copyright (c) 2009, The University of Texas at Austin
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification, 
* are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, 
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice, 
* this list of conditions and the following disclaimer in the documentation and/or other materials 
* provided with the distribution.
*
* Neither the name of The University of Texas at Austin nor the names of its contributors may be 
* used to endorse or promote products derived from this software without specific prior written 
* permission.
* 
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR 
* IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND 
* FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS 
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF 
* THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/  

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Globalization;
using System.Linq;
using System.Windows;
using System.Windows.Input;
using System.Windows.Media;
using Bio.Data.Interfaces;
using Bio.Data.Providers.Interfaces;
using System.Diagnostics;
using System.Threading;
using Bio.Views.Alignment.Models;
using Bio.Views.Alignment.Views;
using Bio.Views.ViewModels;
using JulMar.Windows.Mvvm;
using JulMar.Windows.Interfaces;

namespace Bio.Views.Alignment.ViewModels
{
    /// <summary>
    /// This is the primary view model for the alignment viewer
    /// </summary>
    class AlignmentViewModel : BioViewModel<AlignmentEditor>
    {
        /// <summary>
        /// This field holds the first available (located) non-gap/empty nuceleotide column.  It is used
        /// to hide all starting columns that hold no sequence data when the "HideStartingEmptySequences" option is selected.
        /// </summary>
        private int _firstAvailableNucleotideColumn;

        /// <summary>
        /// This is the Bio loader which retrieves the underlying alignment sequences.
        /// </summary>
        private IBioDataLoader<IAlignedBioEntity> _data;

        /// <summary>
        /// This is the actual data being displayed.
        /// </summary>
        public ObservableCollection<RowViewModel> Rows { get; set; }

        /// <summary>
        /// Command to change the reference sequence colors used
        /// </summary>
        public ICommand ChangeReferenceColorsCommand { get; private set; }

        /// <summary>
        /// This is the list of reference colors
        /// </summary>
        public ObservableCollection<string> ReferenceColors { get; private set; }

        /// <summary>
        /// True/False whether grouping by Taxonomy should be used if available
        /// </summary>
        private bool _displayGroupings;
        public bool DisplayGroupings
        {
            get { return _displayGroupings; }
            set
            {
                if (!SupportsGrouping || (_displayGroupings == value))
                    return;

                _displayGroupings = value; 
                OnPropertyChanged("DisplayGroupings");
                ResetGrouping();
            }
        }

        /// <summary>
        /// True/False whether grouping is supported (requires data provider support)
        /// </summary>
        private bool _supportsGrouping;
        public bool SupportsGrouping
        {
            get { return _supportsGrouping; }
            set { _supportsGrouping = value; OnPropertyChanged("SupportsGrouping"); }
        }

        /// <summary>
        /// Indicates the grouping style requested for taxonomy purposes.
        /// </summary>
        public GroupCollapseStyle MinMaxGroupingStyle { get; set; }

        /// <summary>
        /// The minimum/maximum grouping range
        /// </summary>
        private int _minMaxGroupingRange;
        public int MinMaxGroupingRange
        {
            get { return _minMaxGroupingRange; }
            set
            {
                if (_minMaxGroupingRange == value)
                    return;

                _minMaxGroupingRange = value; 
                OnPropertyChanged("MinMaxGroupingRange");
                ResetGrouping();
            }
        }

        /// <summary>
        /// Selected row in the grid
        /// </summary>
        private RowViewModel _selectedRow;
        public RowViewModel SelectedRow
        {
            get { return _selectedRow; }
            set { _selectedRow = value; OnPropertyChanged("SelectedRow"); }
        }

        /// <summary>
        /// Total number of visible columns based on window size.
        /// </summary>
        private int _columns;
        public int VisibleColumns
        {
            get { return _columns; }
            set { _columns = value; OnPropertyChanged("VisibleColumns"); }
        }

        /// <summary>
        /// Total number of visible rows based on window size.
        /// </summary>
        private int _rows;
        public int VisibleRows
        {
            get { return _rows; }
            set { _rows = value; OnPropertyChanged("VisibleRows"); }
        }

        /// <summary>
        /// Current line height - note we require every line is the same size!
        /// </summary>
        private double _currentWidth, _currentHeight;
        private readonly double _lineHeight;

        /// <summary>
        /// Total number of columns in our data
        /// </summary>
        private int _totalColumns;
        public int TotalColumns
        {
            get 
            {
                if (_totalColumns == 0 && _data != null)
                    _totalColumns = _data.Entities.Max(s => s.AlignedData.Count);
                return _totalColumns;
            }
        }

        /// <summary>
        /// Total number of sequence alignments (rows) in our data.
        /// </summary>
        public int TotalRows
        {
            get { return (GroupedEntity != null) ? GroupedEntity.Count : 0; }    
        }

        /// <summary>
        /// Used to determine the horizontal scrollbar maximum scrolling size.
        /// </summary>
        public int AffectedColumns
        {
            get { return TotalColumns - VisibleColumns; }
        }

        /// <summary>
        /// Used to determine the vertical scrollbar maximum scrolling size
        /// </summary>
        public int AffectedRows
        {
            get { return TotalRows - VisibleRows; }
        }

        /// <summary>
        /// Current position of the horizontal scrollbar (1st visible column)
        /// </summary>
        private int _column;
        public int CurrentColumn
        {
            get { return _column; }
            set { ScrollToPosition((value > 0) ? value : 0, CurrentRow); }
        }

        /// <summary>
        /// Current position of the vertical scrollbar (1st visible row)
        /// </summary>
        private int _row;
        public int CurrentRow
        {
            get { return _row; }
            set { ScrollToPosition(CurrentColumn, (value > 0) ? value : 0); }
        }

        /// <summary>
        /// Title for the window
        /// </summary>
        public override string Title
        {
            get { return string.Format("Alignment View: {0} {1} sequences", base.Title, (_data != null) ? _data.Entities.Count : 0); }    
            set { base.Title = value; }
        }

        /// <summary>
        /// True if the data source supports hiding unused columns
        /// </summary>
        private bool _supportsHidingSequences;
        public bool SupportsHidingSequences
        {
            get { return _supportsHidingSequences; }
            set { _supportsHidingSequences = value; OnPropertyChanged("SupportsHidingSequences"); }
        }

        /// <summary>
        /// Scrolls the first visible column to be the first non-empty cell
        /// </summary>
        private bool _hideStartingEmptySequences;
        public bool HideStartingEmptySequences
        {
            get { return _hideStartingEmptySequences; }
            set
            {
                if (_hideStartingEmptySequences != value)
                {
                    _hideStartingEmptySequences = value;
                    OnPropertyChanged("HideStartingEmptySequences");
                    if (_firstAvailableNucleotideColumn > 0)
                        ScrollToPosition(CurrentColumn, CurrentRow);
                }
            }
        }

        /// <summary>
        /// Constructor
        /// </summary>
        public AlignmentViewModel()
        {
            _column = _row = -1;
            VisibleColumns = VisibleRows = 1;
            SupportsHidingSequences = false;
            MinMaxGroupingStyle = GroupCollapseStyle.Minimum;
            MinMaxGroupingRange = 5;

            ChangeReferenceColorsCommand = new DelegatingCommand(OnChangeRefSeqColors);

            // Fill in default colors.
            // TODO: persist this list somewhere.
            ReferenceColors = new ObservableCollection<string>
              {
                  "LightBlue",
                  "LightGreen",
                  "LightYellow",
                  "LightPink",
                  "LightSeaGreen"
              };

            var ft = new FormattedText("HELLO WORLD", CultureInfo.CurrentCulture, FlowDirection.LeftToRight,
                                        new Typeface("Arial"), 12, Brushes.Black);
            _lineHeight = ft.Height + 2.75;
        }

        /// <summary>
        /// This initializes the view model and generates the appropriate view.
        /// </summary>
        /// <param name="data"></param>
        /// <returns></returns>
        public override bool Initialize(IBioDataLoader data)
        {
            _data = data as IBioDataLoader<IAlignedBioEntity>;
            Debug.Assert(_data != null);
            
            if (_data == null)
                return false;

            IBioDataLoaderProperties bioProps = data as IBioDataLoaderProperties;
            if (bioProps != null)
                bioProps.PropertiesChanged += OnLoaderPropertiesChanged;

            FindFirstNucleotideColumn();

            return _data != null && base.Initialize(data);
        }

        private void FindFirstNucleotideColumn()
        {
            ThreadPool.QueueUserWorkItem((o) =>
                                             {
                                                 int minPos = Int32.MaxValue;
                                                 foreach (var e in _data.Entities)
                                                 {
                                                     int pos = e.FirstDataColumn;
                                                     if (pos >= 0)
                                                         minPos = Math.Min(pos, minPos);
                                                 }
                                                 _firstAvailableNucleotideColumn = minPos < Int32.MaxValue ? minPos : 0;
                                                 SupportsHidingSequences = _firstAvailableNucleotideColumn > 0;
                                             });
        }

        private void OnLoaderPropertiesChanged(object sender, EventArgs e)
        {
            INotificationVisualizer wait = Resolve<INotificationVisualizer>();
            Debug.Assert(wait != null);
            using (wait.BeginWait("Refreshing View", "View has changed - reloading data"))
            {
                _column = _row = -1;
                _totalColumns = 0;
                SupportsHidingSequences = false;
                FindFirstNucleotideColumn();

                Rows.Clear();

                _groupedList = null;
                var ge = GroupedEntity[0];
                OnCreatedView();
                ScrollToPosition(CurrentColumn, CurrentRow);

                // The world just changed.
                OnPropertyChanged(null);
            }
        }

        public void OnViewResized(Size newSize)
        {
            if (_currentWidth == newSize.Width || _currentHeight == newSize.Height)
                return;

            _currentWidth = newSize.Width;
            _currentHeight = newSize.Height;

            if (_currentWidth != 0 && _currentHeight != 0)
                OnCreatedView();

            ScrollToPosition(CurrentColumn, CurrentRow);
        }

        public void ScrollLineUp()
        {
            ScrollToPosition(CurrentColumn, CurrentRow-1);
        }

        public void ScrollLineDown()
        {
            ScrollToPosition(CurrentColumn, CurrentRow + 1);
        }

        public void ScrollLineLeft()
        {
            ScrollToPosition(CurrentColumn-1, CurrentRow);
        }

        public void ScrollLineRight()
        {
            ScrollToPosition(CurrentColumn + 1, CurrentRow);
        }

        public void ScrollPageUp()
        {
            ScrollToPosition(CurrentColumn, CurrentRow - VisibleRows);
        }

        public void ScrollPageDown()
        {
            ScrollToPosition(CurrentColumn, CurrentRow + VisibleRows);
        }

        public void ScrollPageLeft()
        {
            ScrollToPosition(CurrentColumn - VisibleColumns, CurrentRow);
        }

        public void ScrollPageRight()
        {
            ScrollToPosition(CurrentColumn + VisibleColumns, CurrentRow);
        }

        public void ScrollToPosition(int x, int y)
        {
            // Do bounding
            if (x < 0) x = 0;
            if (y < 0) y = 0;
            if (y > (TotalRows-VisibleRows)) y = Math.Max(0, TotalRows - VisibleRows);
            if (x > (TotalColumns-VisibleColumns)) x = Math.Max(TotalColumns - VisibleColumns, 0);

            if (HideStartingEmptySequences && x < _firstAvailableNucleotideColumn)
                x = _firstAvailableNucleotideColumn;

            _column = x; _row = y;
            OnPropertiesChanged("CurrentRow", "CurrentColumn");

            // Update the rows
            int ctr = CurrentRow;
            for (int ri = 0; ri < VisibleRows; ri++)
            {
                var row = Rows[ri];
                if (row.IsLocked)
                    row.UpdateRow(CurrentColumn, VisibleColumns);
                else
                {
                    int rowIndex = ctr++; //CurrentRow + ri;
                    if (rowIndex < TotalRows)
                    {
                        row.UpdateRow(GroupedEntity[rowIndex], CurrentColumn, VisibleColumns);
                    }
                    else
                    {
                        row.UpdateRow(null, 0, VisibleColumns);
                    }
                }
            }
        }

        /// <summary>
        /// This method is called to change the reference sequence colors
        /// </summary>
        private void OnChangeRefSeqColors()
        {
            // TODO: show dialog to change colors
        }

        /// <summary>
        /// This orders the sequence alignment data by taxonomy for grouping purposes.
        /// </summary>
        private List<IAlignedBioEntity> _groupedList;
        private IList<IAlignedBioEntity> GroupedEntity
        {
            get
            {
                if (_groupedList == null && _data.Entities != null)
                {
                    var groupedList = _data.Entities
                                            .OrderBy(r => r.Entity.TaxonomyId)
                                            .GroupBy(r => r.Entity.TaxonomyId);

                    int groupCount = groupedList.Count();
                    SupportsGrouping = groupCount > 1;
                    _groupedList = (SupportsGrouping && DisplayGroupings) 
                        ? new GroupListGenerator(groupedList).Generate(MinMaxGroupingStyle, MinMaxGroupingRange) 
                        : _data.Entities.OrderBy(r => r.Entity.ScientificName).ToList();

                    OnPropertiesChanged("TotalRows");
                }
                return _groupedList;
            }
        }

        public void ChangeReferenceSequenceSelection(RowViewModel model)
        {
            Parallel.ForEach(Rows, row =>
               {
                   if (row != model)
                       row.NewReferenceSequenceSelected(model);
               });
        }

        private void ResetGrouping()
        {
            if (_groupedList != null)
            {
                _groupedList = null;
                var ge = GroupedEntity[0];
                OnCreatedView();
                ScrollToPosition(CurrentColumn, CurrentRow);
            }
        }

        protected override void OnCreatedView()
        {
            if (_currentHeight > 0)
                VisibleRows = ((int)(_currentHeight / _lineHeight)) - 1;

            if (Rows == null)
                Rows = new ObservableCollection<RowViewModel>();

            VisibleColumns = (int)((_currentWidth - 220) / 20);
            if (VisibleColumns <= 0)
                VisibleColumns = 1;

            foreach (var row in Rows)
                row.ResetColumnCount(VisibleColumns);

            if (Rows.Count < VisibleRows)
            {
                for (int i = Rows.Count; i < VisibleRows; i++)
                    Rows.Add(new RowViewModel(this, VisibleColumns));
            }
            else if (Rows.Count > VisibleRows)
            {
                int removeCount = Rows.Count - VisibleRows;
                for (int i = 0; i < removeCount; i++)
                    Rows.RemoveAt(Rows.Count - 1);
            }

            OnPropertiesChanged("TotalRows", "AffectedRows", "TotalColumns", "AffectedColumns", "Title", "VisibleRows", "VisibleColumns");
        }

        /// <summary>
        /// This method is used to push all locked rows to the top of the display.
        /// </summary>
        /// <param name="model"></param>
        public void OnRowLocked(RowViewModel model)
        {
            if (model.IsLocked)
            {
                for (int i = 0; i < Rows.Count; i++)
                {
                    if (Rows[i].IsLocked == false)
                    {
                        Rows.Remove(model);
                        Rows.Insert(i, model);
                        break;
                    }
                }
            }
            else
            {
                int pos = Rows.IndexOf(model);
                for (int i = pos+1; i < Rows.Count; i++)
                {
                    if (Rows[i].IsLocked == true)
                    {
                        Rows.Remove(model);
                        Rows.Insert(i, model);
                        break;
                    }
                }
            }

            ScrollToPosition(CurrentColumn, CurrentRow);
        }
    }
}